def simulate(warehouse, items, actions, observers=()):
    """This function basically executes the given actions sequentially in a copy of the warehouse,
    letting observers run arbritrary code right before and after executing each action, thus 
    allowing them to extract data that is dependent on the evolution of the warehouse along time.
    This function returns the values returned by the observers' end() method."""
    # make a copy of the warehouse and initialize observers 
    warehouse = warehouse.deepcopy()
    warehouse.items = dict((i.id, i) for i in items)
    for observer in observers:
        observer.init(warehouse)
        
    # execute all the actions sequentially, calling  
    # pre_execute() and post_execute() on all observers
    for action in actions:
        crane = warehouse.objects[action.meta.crane]
        for observer in observers:
            observer.pre_execute(crane, action)
        crane.execute(action)
        for observer in observers:
            observer.post_execute(crane, action)
    
    # finalize observers and collect results
    results = list()
    for observer in observers:
        result = observer.end()
        results.append(result if result is not None else observer)
    return results[0] if len(results) == 1 else results
    
    
class SimObserver(object):
    """Base class for simulation observers. A simulation observer is an object that gathers 
    information during the execution of a simulation (i.e. executing a sequence of actions on a 
    particular warehouse) to compute some indicator that can only be inferred by observing the 
    evolution of the warehouse's state with time."""
    def init(self, warehouse):
        self.warehouse = warehouse
        self.items = warehouse.items
        
    def pre_execute(self, crane, action):
        pass
        
    def post_execute(self, crane, action):
        pass
        
    def end(self):
        pass
        
        
